fix(middleman): resolve broadcast failures through the verifier - #342
Conversation
A broadcast with no clean answer was written `Failure` with no hash: invisible to the verifier forever, though the tx may have landed. `sendTransaction` now derives the hash locally (sha256 of the bytes it broadcasts, as `matchTxInBlock` does), so every outcome carries one. It also stops flattening errors: dedup is success, a deterministic CheckTx rejection is `rejected`, timeouts and resets are unknown. `ExecuteTransaction` anchors hash + heights BEFORE broadcasting; an unknown outcome stays `pending` for the chain to settle.
- executeTransaction now throws a retryable failure when the node gave no definitive answer, so Temporal re-sends the same bytes on a policy scoped to that activity; a sdk code 32 is handed to the verifier instead. - Both flows catch the exhausted retry: the anchored flow records the last error, the legacy flow anchors the row so a relaunch can never re-send as attempt 1 and trust a rejection while the stake is on chain. - sendTransaction classifies refused/unresolvable connections as neverSent (walking undici's cause chain), drops the dead TimeoutError branch, and the attempt number defaults to null outside an activity context. - The failure type lives in a Node-free leaf module and a bundle smoke test guards it: importing it from the activities module broke the workflow bundle. Activity, DAL and classifier tests added.
PR description — fixes #339SummaryA broadcast that returned no clean answer was recorded as a permanent The broadcaster no longer decides the outcome. It anchors the transaction (hash + heights), sends Design
Broadcast retriesA tx signed through Soothe carries no
The blocker that had to be fixed firstThe anchor could not have worked as intended. Both consumers were affected, so Measured against the real function with a real Without this, the change would have moved the defect rather than removed it: from "wrongly marked Also included
Correction to the issue textThe issue says the middleman never got the #308 broadcast/verify split. It got half — the Tests
|
### Reliability - A broadcast with no clean answer is no longer written as a permanent failure. The hash is derived locally so every outcome carries one, an unknown outcome is retried with the identical bytes, and when the retries run out the anchored row is handed to the verifier instead of failing the run. The verifier can now also reach failure verdicts at all: signer and sequence parsing read the stored hex payload as base64. (#342) ### Security - The provider's pending dispatcher selects ids only. Whole rows used to cross the activity boundary, so every pending transaction's params, which carry the signer private key in cleartext since the encrypted column is decrypted on read, were persisted in Temporal workflow history. (#350) - Database errors are mapped to safe messages at the action boundaries instead of sending "Failed query: <sql> params: <values>" to the browser. (#344) ### Transactions - Unstakes record the amount they release, so Total POKT is no longer 0.00; a row with no recorded amount shows "—" instead of a zero that looks like the bug. (#347) ### Notifications - Header banners are replaced by a topbar bell feed. Failed actions still raise a short toast where the click happened; the in-app feed opt-out and the history channel filter are removed on purpose. (#344) ### Provider - Public GET /api/identity serves the key the running instance holds, so governance CI can catch a registry entry pointing at an instance with a different key. (#337) ### UI - Wide content no longer pushes the whole page sideways. (#345)
A broadcast with no clean answer was written
Failurewith no hash: invisible to the verifier forever, though the tx may have landed.sendTransactionnow derives the hash locally (sha256 of the bytes it broadcasts, asmatchTxInBlockdoes), so every outcome carries one.It also stops flattening errors: dedup is success, a deterministic CheckTx rejection is
rejected, a refused or unresolvable connection isneverSent(nothing reached the node), and everything else is unknown.ExecuteTransactionanchors hash + heights BEFORE broadcasting; an unknown outcome stayspendingfor the chain to settle.Unknown outcomes are retried, not parked.
executeTransactionreturns only a definitive answer: success, a CheckTx rejection, or a sdk code 32 (sequence already consumed, which a re-send cannot change and the verifier settles by hash or by the sequence rule). Anything else — node unreachable, connection dropped mid-request, mempool full — throws a retryable failure so Temporal re-broadcasts the same bytes, on a retry policy scoped to that one activity (5s → 30s backoff, 5 attempts, ~3.5 min worst case). This closes the gap for Soothe-signed transactions, which carry no timeoutHeight and would otherwise sit pending, keys held, until the signer's sequence was consumed by some other tx. Once the policy is spent, both flows catch the failure: the anchored flow records the last error on the row, and the legacy flow (pre-upgrade histories) anchors the row in that run — never failing the run, since a relaunched run with no hash would re-send as attempt 1 and could trust a rejection while the stake is on chain. An outage longer than the retry window still leaves a Soothe-signed row waiting on the sequence rule; an alert for pending rows with no timeout is a follow-up.parseSignerAndSequencedecoded hex payloads as base64 and returned nulls for every transaction, so no middleman transaction could reach a failure verdict; fixed and tested both ways.The workflow bundle is now guarded by a test that builds it: a runtime import from the activities module would pull drizzle/pg/node:crypto into Temporal's sandbox and crash the worker at start-up, which nothing else in CI can see.
Closes #339
Not done, flagged for follow-up:
A Soothe-signed tx carries no
timeoutHeight, so a broadcast that never reached a mempool canonly be failed once the signer's sequence is consumed by another tx — for an idle delegator,
never. The retries here shrink the window to ~3.5 min but do not close it. Alerting on those
rows (never auto-failing them) is #351.